缓冲区是数据的高级抽象。缓冲区不必绑定到单个位置或虚拟内存地址上。运行时可以使用内存中的许多位置(甚至跨不同设备)来表示缓冲区，但运行时必须提供一致的数据视图。程序在主机和任何设备上都可以访问缓冲区。\par

\hspace*{\fill} \par %插入空行
图7-1 缓冲区类型的定义
\begin{lstlisting}[caption={}]
template <typename T, int Dimensions, AllocatorT allocator>
class buffer;
\end{lstlisting}

buffer类是一个模板类，有三个模板参数，如图7-1所示。第一个模板参数是缓冲区包含的对象的类型，这个类型必须可复制，从而可以安全地逐字节地复制，而不需要使用特殊的copy或move函数。下一个模板参数是描述缓冲区维度的数量。模板的最后一个参数是可选的，通常使用默认值。此参数指定一个分配器类，用于在主机上分配缓冲区所需的内存。首先，我们来看下究创建缓冲区对象的方式。\par

\hspace*{\fill} \par %插入空行
\textbf{创建}

下图中，展示了创建缓冲区对象的几种方式。如何在程序代码中创建缓冲区决定于使用缓冲区的方式和个人偏好。浏览一下这个示例的每个实例。\par

\hspace*{\fill} \par %插入空行
图7-2 创建缓冲区，第1部分
\begin{lstlisting}[caption={}]
// Create a buffer of 2x5 ints using the default allocator
buffer<int, 2, buffer_allocator> b1{range<2>{2, 5}};

// Create a buffer of 2x5 ints using the default allocator 
// and CTAD for range
buffer<int, 2> b2{range{2, 5}};

// Create a buffer of 20 floats using a 
// default-constructed std::allocator
buffer<float, 1, std::allocator<float>> b3{range{20}};

// Create a buffer of 20 floats using a passed-in allocator
std::allocator<float> myFloatAlloc;
buffer<float, 1, std::allocator<float>> b4{range(20), myFloatAlloc};
\end{lstlisting}

图7-2中创建的第一个缓冲区b1，是一个包含10个整数的二维缓冲区。显式传递所有模板实参，显式传递buffer\_allocator的默认值作为分配器类型，使用现代C++可以更简单地表达，缓冲区b2也是包含10个整数的二维缓冲区。这里，使用C++17的类模板实参推断(CTAD)来自动推断模板实参。\par

CTAD需要推断一个类的每个模板参数，或者一个也不推断。本例中，使用带两个参数的range初始化b2，来确定它是一个二维的range。allocator模板参数有一个默认值，所以在创建缓冲区时不需要显式地列出。\par

对于缓冲区b3，创建了一个包含20个浮点数的缓冲区，并使用默认构造的std::allocator<float>来分配主机上的内存。当自定义分配器类型与缓冲区一起使用时，通常希望将实际的分配器对象传递给缓冲区使用，而不是默认构造的分配器对象。b4展示了如何做到这一点。\par

对于示例中的前四个缓冲区，让缓冲区分配需要的内存，并且在创建数据时不使用任何值初始化数据。使用缓冲区封装内存分配是一种常见的模式，这些内存可能已经初始化了。可以通过向缓冲区构造函数传递初始值进行初始化。\par

\hspace*{\fill} \par %插入空行
图7-3 创建缓冲区，第2部分
\begin{lstlisting}[caption={}]
// Create a buffer of 4 doubles and initialize it from a host pointer
double myDoubles[4] = {1.1, 2.2, 3.3, 4.4};
buffer b5{myDoubles, range{4}};

// Create a buffer of 5 doubles and initialize it from a host pointer 
// to const double
const double myConstDbls[5] = {1.0, 2.0, 3.0, 4.0, 5.0};
buffer b6{myConstDbls, range{5}};

// Create a buffer from a shared pointer to int
auto sharedPtr = std::make_shared<int>(42);
buffer b7{sharedPtr, range{1}};
\end{lstlisting}

图7-3中，b5创建了一个具有4个double的一维缓冲区。除了指定缓冲区大小的范围外，还将指向C数组mydouble的主机指针传递给缓冲区构造函数。这里，可以充分利用CTAD来推断缓冲区的所有模板参数。缓冲区的数据，是通过传递double的主机指针获取。维数是自动从一维range中推断出来的，而这个range是由一个数字创建的。最后，使用了默认分配器，因此不必进行指定。\par

传递主机指针有几个要点。通过传递指向主机内存的指针，在缓冲区的生命周期内，不会尝试访问主机内存。这不是(也不能)由SYCL实现强制执行的——确保不违反此契约是我们的责任。当缓冲区存在时，不应该尝试访问该内存的原因是，缓冲区可能会选择使用主机上的不同内存来表示缓冲区内容，这通常出于优化的考虑。如果这样做，值将从主机指针复制到新内存中。如果后续的内核修改缓冲区，原始的主机指针将不会更新其内容，直到特定的同步点。本章的后面，我们将更多地讨论数据何时会写回主机指针。\par

b6与b5非常相似，但有一个区别，使用指向const double的指针初始化缓冲区。我们只能通过宿主指针读取值，而不能写。但是，本例中缓冲区的类型是double，而不是const double，因为推导时没有考虑到常量。这意味着内核可能会修改缓冲区内的数据，所以需要使用不同的机制来更新主机缓冲区(在本章后面讨论)。\par

可以使用C++共享内存对象初始化缓冲区。如果程序已经使用了共享内存，这种初始化方法将正确地计算引用，并确保内存不会释放。b7使用一个整数初始化缓冲区维度，并使用共享内存初始化缓冲区数据。\par

\hspace*{\fill} \par %插入空行
图7-4 创建缓冲区，第3部分
\begin{lstlisting}[caption={}]
// Create a buffer of ints from an input iterator
std::vector<int> myVec;
buffer b8{myVec.begin(), myVec.end()};
buffer b9{myVec};

// Create a buffer of 2x5 ints and 2 non-overlapping 
// sub-buffers of 5 ints.
buffer<int, 2> b10{range{2, 5}};
buffer b11{b10, id{0, 0}, range{1, 5}};
buffer b12{b10, id{1, 0}, range{1, 5}};
\end{lstlisting}

容器通常在现代C++应用程序中使用，例如：std::array、std::vector、std::list或std::map。可以用两种不同的方式使用容器初始化一维缓冲区。第一种方法，如图7-4所示，使用b8和使用输入迭代器。将两个迭代器传递给缓冲区构造函数，一个表示数据的开始，另一个表示结束。通过递增start迭代器，直到等于end迭代器所返回的元素个数来计算缓冲区的大小。这对于任何实现C++输入迭代器接口的数据类型都很有用。如果为缓冲区提供初始值的容器对象是连续的，可以使用更简单的方式来创建缓冲区。b9通过将vector传递给构造函数来vector创建缓冲区。缓冲区的大小由初始化容器的大小决定，缓冲区数据的类型源于容器内的数据类型，建议在使用std::vector和std::array时，这样创建缓冲区。\par

缓冲区创建的最后一个示例说明了缓冲区类的另一个特性。可以从另一个缓冲区或子缓冲区创建缓冲区。子缓冲区需要三样东西:对父缓冲区的引用、基索引和子缓冲区的范围。不能从子缓冲区创建子缓冲区，同一个缓冲区可以创建多个子缓冲区，可以自由重叠。b10的创建方式与b2完全相同，是一个每行5个整数的二维缓冲区。我们从b10、子缓冲区b11和b12创建两个子缓冲区。子缓冲区b11从index(0,0)开始，包含第一行中的每个元素。类似地，子缓冲区b12从index(1,0)开始，包含第二行中的每个元素。这将产生两个不相交的子缓冲区。由于子缓冲区不重叠，不同的内核可以同时对不同的子缓冲区进行操作，我们会在下一章更多地讨论调度执行图和依赖性相关的话题\par

\hspace*{\fill} \par %插入空行
图7-5 缓冲区属性
\begin{lstlisting}[caption={}]
queue Q;
int my_ints[42];

// create a buffer of 42 ints
buffer<int> b{range(42)};

// create a buffer of 42 ints, initialize 
// with a host pointer, and add the 
// use_host_pointer property
buffer b1{my_ints, range(42),
	{property::buffer::use_host_ptr{}}};

// create a buffer of 42 ints, initialize pointer,
// with a host and add the use_mutex property
std::mutex myMutex;
buffer b2{my_ints, range(42), 
	{property::buffer::use_mutex{myMutex}}};

// Retrive a pointer to the mutex used by this buffer
auto mutexPtr =
	b2.get_property<property::buffer::use_mutex>().
		get_mutex_ptr();
		
// lock the mutex until we exit scope
std::lock_guard<std::mutex> guard{*mutexPtr};

// create a context-bound buffer of 42 ints, 
// initialized from a host pointer
buffer b3{my_ints, range(42), 
	{property::buffer::context_bound{Q.get_context()}}};
\end{lstlisting}

\hspace*{\fill} \par %插入空行
\textbf{缓冲区属性}

还可以用特殊属性创建缓冲区，以改变缓冲区行为。图7-5中，演示三个不同的可选缓冲区属性的示例，并讨论如何使用它们。注意，这些属性在大多数代码中相对不常见。\par

\hspace*{\fill} \par %插入空行
\textbf{use\_host\_ptr}

在缓冲区创建期间可以选择性的指定第一个属性是use\_host\_ptr，此属性要求缓冲区不在主机上分配任何内存，并且在缓冲区构造上传递或指定的任何分配器都会忽略。缓冲区必须使用传递给构造函数的主机指针所指向的内存。注意，这并不需要设备使用相同的内存来保存缓冲区的数据。设备可以自由地将缓冲区内容缓存到存储器中。还有，此属性只能在主机指针传递给构造函数时使用。当程序希望完全控制所有主机内存分配时，这个选项很有用。\par

图7-5中，我们创建了一个缓冲区b。接下来，我们创建缓冲区b1，并用指向myint的指针初始化它。还传递了属性use\_host\_ptr，这意味着缓冲区b1将只使用myint所指向的内存，而不会分配任何额外的存储空间.\par

\hspace*{\fill} \par %插入空行
\textbf{use\_mutex}

use\_mutex关注缓冲区和主机代码之间的细粒度内存共享。缓冲区b2使用此属性创建，该属性采用对互斥对象的引用，该对象可以从缓冲区中查询。此属性还要求将主机指针传递给构造函数，并让运行时确定何时可以安全地通过提供的主机指针，访问主机代码中的数据。运行时保证主机指针能看到缓冲区的值之前，不能锁定互斥锁。虽然可以与use\_host\_ptr属性结合使用，但这不是必需的。use\_mutex是一种机制，允许主机代码在缓冲区活跃的情况下访问缓冲区中的数据，而不使用主机访问器机制(稍后介绍)。除非有特定的理由使用互斥锁，否则应该优先使用主机访问器机制，特别是在互斥锁成功锁定和主机代码使用数据之前。\par

\hspace*{\fill} \par %插入空行
\textbf{context\_bound}

最后一个属性展示在示例中创建缓冲区b3的过程中。42个整数的缓冲区用context\_bound属性创建，该属性接受对上下文对象的引用。缓冲区可以在任何设备或上下文上使用。如果使用此属性，则将缓冲区绑定到指定的上下文，试图在另一个上下文上使用缓冲区将导致运行时错误。这对于调试程序很有帮助，例如：通过识别内核可能提交到错误队列的情况，可以确定错误产生的位置。实际中，我们不想在许多程序中看到这个属性，并且上下文在任何设备上访问缓冲区的能力是缓冲区最强大的属性之一(这个属性可以撤消)。\par

\hspace*{\fill} \par %插入空行
\textbf{可以用缓冲区做什么?}

可以用缓冲区做很多事情，查询缓冲区的特征，确定缓冲区销毁后是否，以及在哪里有数据写回主机内存，或者将缓冲区重新解释为具有不同特征的缓冲区。然而，不能直接访问缓冲区的数据，必须创建访问器对象来访问数据。\par

可以查询缓冲区的示例包括范围、数据元素的总数，以及存储元素所需的字节数。还可以查询缓冲区正在使用哪个分配器对象，以及该缓冲区是否为子缓冲区。\par

缓冲区销毁时更新主机内存，根据缓冲区创建的方式，在缓冲区销毁后，主机内存可能会更新。如果缓冲区是从指向非const数据的主机指针创建并初始化的，那么当缓冲区销毁时，该指针将使用已更新的数据。然而，还有一种方法可以更新主机内存，而不管缓冲区如何创建。set\_final\_data方法是缓冲区的模板方法，可以接受指针、C++输出迭代器或std::weak\_ptr。缓冲区销毁时，数据将写入主机。注意，如果缓冲区是从指向非const数据的主机指针创建并初始化，就好像是用该指针调用了set\_final\_data。从技术上讲，指针是输出迭代器的特例。如果传递给set\_final\_data的参数是一个std::weak\_ptr，当指针已经过期或已经删除，则数据不会写入主机。是否发生回写也可以由set\_write\_back控制。\par





























